#!/usr/bin/env python
# encoding: utf-8

"""
@author: wuyang
@software: PyCharm
@file: dir-real-rsync.py
@time: 2017/10/25 8:23
@文件描述：监控目录，并同步有变动的文件（add,modifile,也可以设置delete）
需要设置同步主机的公钥，免秘钥登录。
问题1：pynotify在监控的时候，如果在监控目录下新增目录，则新增目录内部再监控
如：监控目录/homne/test ，在test目录下创建目录dir会监控到create，再在dir下创建新的文件和目录，则无法监控到
临时解决方法：重启当前服务
问题2：批量测试发现，当同时产生大量文件的时候可能会导致监控队列溢出的异常。inotify报的错误
可以考虑直接写入到文件或者redis中，再消费掉，以满足大量文件生成时的快速处理需求
也可以考虑增加inotify的监控队列
"""
import pyinotify

import time
import os
import subprocess
import settings
import logging
import logging.config

# 初始化日志配置
logging.config.dictConfig(settings.logging_conf)
logger = logging.getLogger('common_logger')


class MonitMainHander(pyinotify.ProcessEvent):
    def __init__(self, source_path, des_ip, des_port, des_path):
        logger.debug("开始实例化MonitMainHander")
        pyinotify.ProcessEvent.__init__(self)  # 继承父类的初始化方法参数
        self.source_path = source_path  # 被监控目录
        self.des_ip = des_ip  # 同步的主机ip地址
        self.des_port = des_port  # 目标主机端口
        self.des_path = des_path  # 同步到的目标路径
        # 同步目录初始化
        self.init_command = 'rsync -ae "ssh -p %s" --timeout=60   %s %s:%s -b --exclude=.*' % (
            des_port, source_path, des_ip, des_path)
        logger.debug(self.init_command)
        self._exec_command(self.init_command)
        logger.debug("实例化MonitMainHander结束")

    # 获取被同步文件绝对路径的方法
    @staticmethod
    def _get_absolute_des_path(absolute_source_path, source_path, des_path):
        relative_path = os.path.relpath(absolute_source_path, start=source_path)  # 获取相对路径的方法

        get_path = os.path.join(des_path, relative_path)
        if os.path.isdir(absolute_source_path):
            absolute_des_path = os.path.dirname(get_path)  # 如果是目录，讲目标目录设置成上一级目录
        else:
            absolute_des_path = get_path  # 拼接成需要同步的目录方法
        logger.info("absolute_des_path：" + absolute_des_path)
        return absolute_des_path

    # 执行命令的方法
    @staticmethod
    def _exec_command(command):
        logger.info("组装获取到命令" + command)
        try:
            logger.info("开始执行修改同步命令")
            subprocess.check_call(command, shell=True)
            logger.info("命令执行结束")
        except Exception as e:
            logger.error(e)

    # 检查文件类型的装饰器
    def check_filetype(function):
        def filetype(self, event):
            checkfilepath = os.path.join(event.path, event.name)
            checkcommand = '/usr/bin/file -b --mime-type %s' % checkfilepath
            filetype = subprocess.check_output(checkcommand, shell=True).decode().strip(
                "\n")  # 获取返回bytes 转换为string并去掉换行
            if (filetype != 'text/plain'):
                logger.warning(
                    "发现{}文件不是纯文本文件文件类型为：{}".format(checkfilepath, filetype))  # 此处可以修改成想要的操作，例如不执行下面的方法、或者发出告警
            function(self, event)

        return filetype

    # 组装同步命令方法 这里端口写死了
    # 不同步.* 以.开头文件，一般为传输缓存文件，command命令可酌情配置，满足不不同场景需要
    # --suffix= 如有重名，需要先备份再同步，备份文件名用添加日期后缀
    # 如果同步的是目录，需要在路径后面添加斜杠
    def rsync_command(self, source_path, des_port, des_ip, des_path, event):
        current_time = time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime())
        absolute_source_path = os.path.join(event.path, event.name)
        logger.info("有变化文件：" + absolute_source_path)
        absolute_des_path = self._get_absolute_des_path(absolute_source_path, source_path, des_path)
        command = 'rsync -ae "ssh -p %s" --timeout=60   %s %s:%s -b --exclude=.* --suffix=' % (
            des_port, absolute_source_path, des_ip, absolute_des_path) + current_time
        return command

    @check_filetype  # 可以做文件类型的检查
    def process_IN_MODIFY(self, event):  # 文件发生变化的时候同步（包含了创建和修改）
        command = self.rsync_command(source_path=self.source_path, des_ip=self.des_ip, des_port=self.des_port,
                                     des_path=self.des_path, event=event)
        self._exec_command(command)

    # 如果是新增，判断是否为目录，是目录就同步，否则跳过
    def process_IN_CREATE(self, event):
        file = os.path.join(event.path, event.name)
        if os.path.isdir(file):
            command = self.rsync_command(source_path=self.source_path, des_ip=self.des_ip, des_port=self.des_port,
                                     des_path=self.des_path, event=event)
            logger.info("获取新增目录同步：" + command)
            self._exec_command(command)
        else:
            logger.info("新增的是文件:"+ file )
            pass




    def process_IN_DELETE(self, event):  # 文件发生删除操作的时候（根据情况添加）注意删除的时候可能被把备份文件也删除
        # command = self.rsync_command(source_path=self.source_path,des_ip=self.des_ip,des_port=self.des_port,des_path=self.des_path)
        # os.system(command)
        pass


# source_path:被监控目录
# des_ip: 同步目标ip地址
# des_port: 同步目标ip端口
# des_path: 同步目的路径
def do_monit(source_path, des_ip, des_path, des_port="22"):
    wm = pyinotify.WatchManager()  # create a watchmanager()
    mask = pyinotify.IN_DELETE | pyinotify.IN_MODIFY| pyinotify.IN_CREATE  # 需要监控的事件（创建空的文件此处就不同步了）
    notifier = pyinotify.Notifier(wm, MonitMainHander(source_path, des_ip, des_port, des_path))  # 监控事件通知处理
    wdd = wm.add_watch(source_path, mask, rec=True)  # 加入监控，mask，rec递归
    try:
        # 防止启动多个的命令 设置进程号文件就可以防止启动多个
        notifier.loop(daemonize=settings.daemonize, pid_file=settings.pid_file)
    except pyinotify.NotifierError as err:
        logger.error(err)


if __name__ == "__main__":
    do_monit(source_path=settings.source_path, des_ip=settings.des_ip, des_path=settings.des_path,
             des_port=settings.des_port)
